package demo.db.redis.jedis.monitor;

import org.junit.Test;

import demo.db.redis.jedis.JedisDemo;
import redis.clients.jedis.Jedis;

/**
 * <h1>内存管理</h1>
 * <h2>设置内存上限</h2> Redis通过maxmemory参数限制最大可用内存。限制内存目的主要有：
 * <li># 用于缓存场景，当超出内存上限maxmemory时候使用LRU等删除策略释放空间
 * <li># 防止所用内存超过服务器物理内存
 * <p>
 * 需要注意的是，maxmeory限制的是Redis实际使用的内存量，也就是used_memory统计项对应的内存。
 * 通过设置上限，可以方便实现一台服务器部署多个Redis进程的内存控制。比如一台32G的内存的服务器，预留4GB内存给系统，预留4GB给Redis
 * fork进程， 留给Redis24GB内存，这样就可以部署3个maxmemory=8GB的redis进程。
 * <h2>2.2 动态调整内存上限</h2> 可以通过命令config set maxmemory8GB
 * <h2>内存回收策略</h2> 内存回收机制主要体现在以下两个方面：
 * <h3>2.3.1删除过期的key</h3> Redis采用惰性删除和定时任务删除机制实现过期key的内存回收
 * <li># 惰性删除：用于当客户端读取带有超时属性的key的时候，如果已经超过设置的过期时间，会执行删除操作并返回空。
 * 但是有一个问题，当过期键一直没有访问将无法得到及时删除，从而导致内存 不能及时释放
 * <li># 定时任务删除： Redis内部维护一个定时任务，默认每秒运行10次，通过配置hz属性控制。
 * <h3>2.3.2 内存使用达到maxmeory上限时触发内存溢出的控制策略</h3>
 * 当Redis所用内存达到maxmeory上限时，会触发相应的溢出控制策略。具体策略受maxmeory-policy参数控制，Redis支持6种策略：
 * <li># volatile-lru -> 根据LRU算法删除设置了超时属性的键，直到腾出足够空间为止
 * <li># allkeys-lru -> 根据LRU算法删除键，不管有没有设置超时属性，直到腾出足够空间为止
 * <li># volatile-random -> 随即删除过期键，直到腾出足够空间为止
 * <li># allkeys-random -> 随即删除所有键，直到腾出足够空间为止
 * <li># volatile-ttl -> 根据ttl属性，删除最近将要过期的数据，如果没有回退到noeviction策略
 * <li># noeviction -> 不会删除任何数据，拒绝所有写入操作，并返回错误信息，此时只是响应读
 * 
 * <h1>三 内存优化</h1>
 * <li>3.1 redisObject对象
 * <li>3.2 缩减键值对象 降低Redis内存使用最直接的方式就是缩减key和value的长度
 * 
 * # key的设计：越短越好，如user:{userid}:friends:notify:{fid}，可以简化为u:{uid}:fs:nt:{fid}
 * 
 * # value： 值对象缩减比较复杂，常见的需求是把业务对象序列化放入Redis。
 * <li>3.3 共享对象池
 * <li>3.4 字符串优化
 * <li>3.5 编码优化
 * <li>3.6 控制键的数量： 比如通过hash数据结构就可以减少key的数量，从而减少了RTT的时间，还可以减少key，这样也节约了内存
 * 
 * @author junying.han
 *
 */
public class RedisInfo {

    /**
     * redis存活监控
     * (redis_alive):redis本地监控agent使用ping，如果指定时间返回PONG表示存活，否则redis不能响应请求，可能阻塞或死亡。
     * <li>./redis-cli -c -p 7000 ping | grep -c PONG
     */
    @Test
    public void ping() {
        try (Jedis jedis = JedisDemo.newJedis()) {
            System.out.println("----------------------------命令：ping");
            System.out.println(jedis.ping());
        }
    }

    /**
     * 获取服务端信息
     * 
     * <pre>
     * # Server
    redis_version:5.0.5
    redis_git_sha1:00000000
    redis_git_dirty:0
    redis_build_id:69069e2669a2b8b3
    redis_mode:standalone
    os:Linux 2.6.32-358.6.2.el6.x86_64 x86_64
    arch_bits:64
    multiplexing_api:epoll
    atomicvar_api:sync-builtin
    gcc_version:4.4.7
    process_id:1354
    run_id:0145514b9a0acaf6c4838d9ed8518b55c93b98e8
    tcp_port:6380
    uptime_in_seconds:26674200
    uptime_in_days:308
    hz:40
    configured_hz:10
    lru_clock:7179503
    executable:/usr/local/bin/redis-server
    config_file:/etc/redis/6380.conf
    
    # Clients
    connected_clients:4762
    client_recent_max_input_buffer:133453
    client_recent_max_output_buffer:0
    blocked_clients:0
    
    # Memory
    used_memory:233661368
    used_memory_human:222.84M
    used_memory_rss:284086272
    used_memory_rss_human:270.93M
    used_memory_peak:2000270288
    used_memory_peak_human:1.86G
    used_memory_peak_perc:11.68%
    used_memory_overhead:96649892
    used_memory_startup:6216264
    used_memory_dataset:137011476
    used_memory_dataset_perc:60.24%
    allocator_allocated:233758392
    allocator_active:321314816
    allocator_resident:358477824
    total_system_memory:8253054976
    total_system_memory_human:7.69G
    used_memory_lua:216064
    used_memory_lua_human:211.00K
    used_memory_scripts:123504
    used_memory_scripts_human:120.61K
    number_of_cached_scripts:119
    maxmemory:2000000000
    maxmemory_human:1.86G
    maxmemory_policy:allkeys-lru
    allocator_frag_ratio:1.37
    allocator_frag_bytes:87556424
    allocator_rss_ratio:1.12
    allocator_rss_bytes:37163008
    rss_overhead_ratio:0.79
    rss_overhead_bytes:-74391552
    mem_fragmentation_ratio:1.22
    mem_fragmentation_bytes:50446608
    mem_not_counted_for_evict:0
    mem_replication_backlog:0
    mem_clients_slaves:0
    mem_clients_normal:81106916
    mem_aof_buffer:0
    mem_allocator:jemalloc-5.1.0
    active_defrag_running:0
    lazyfree_pending_objects:0
    
    # Persistence
    loading:0
    rdb_changes_since_last_save:4528
    rdb_bgsave_in_progress:0
    rdb_last_save_time:1601014920
    rdb_last_bgsave_status:ok
    rdb_last_bgsave_time_sec:2
    rdb_current_bgsave_time_sec:-1
    rdb_last_cow_size:33951744
    aof_enabled:0
    aof_rewrite_in_progress:0
    aof_rewrite_scheduled:0
    aof_last_rewrite_time_sec:-1
    aof_current_rewrite_time_sec:-1
    aof_last_bgrewrite_status:ok
    aof_last_write_status:ok
    aof_last_cow_size:0
    
    # Stats
    ... 参考 infoStats
    
    # Replication
    role:master
    connected_slaves:0
    master_replid:a295f2a38f0ab09c02e009491e1cf97d283504da
    master_replid2:0000000000000000000000000000000000000000
    master_repl_offset:10787373
    second_repl_offset:-1
    repl_backlog_active:0
    repl_backlog_size:1048576
    repl_backlog_first_byte_offset:9738798
    repl_backlog_histlen:1048576
    
    # CPU
    used_cpu_sys:529596.360115
    used_cpu_user:289541.310030
    used_cpu_sys_children:71671.431286
    used_cpu_user_children:267823.437648
    
    # Cluster
    cluster_enabled:0
    
    # Keyspace
    db0:keys=25743,expires=13496,avg_ttl=91979717
    db1:keys=16279,expires=14267,avg_ttl=32123778
    db2:keys=2829,expires=1210,avg_ttl=463421936
     * </pre>
     */
    @Test
    public void info() {
        try (Jedis jedis = JedisDemo.newJedis()) {
            System.out.println("----------------------------命令：info");
            System.out.println(jedis.info());
        }
    }

    /**
     * 获取服务端信息
     */
    @Test
    public void infoServer() {
        try (Jedis jedis = JedisDemo.newJedis()) {
            System.out.println("----------------------------命令：info Server");
            System.out.println(jedis.info("Server"));
        }
    }

    /**
     * 客户端连接数， 这个值可以通过info clients中的字段connected_clients获取，
     * 它会受到操作系统ulimit和redis的maxclients配置的限制。 如果Rdis客户端中报出获取不到连接数的错误（异常信息：ERR max
     * number of clients reached）， 需要排查这两个地方是否限制了客户端连接数。
     * 当然，也可能还有其他其他原因，比如客户端BUG导致连接没有释放等。
     * <li>连接个数 (connected_clients)：客户端连接个数，如果连接数过高，影响redis吞吐量。>5000 时告警
     * <li>list阻塞调用被阻塞的连接个数 (blocked_clients): 如果监控数据大于0，告警
     * 
     * <pre>
     * # Clients
    connected_clients:4763
    client_recent_max_input_buffer:2
    client_recent_max_output_buffer:0
    blocked_clients:0
     * </pre>
     */
    @Test
    public void infoClients() {
        try (Jedis jedis = JedisDemo.newJedis()) {
            System.out.println("----------------------------命令：info Clients");
            System.out.println(jedis.info("Clients"));
        }
    }

    /**
     * Redis高性能保障的一个重要资源就是足够的内存。Used memory表示Redis已经分配的总内存大小。我们可以通过info
     * memory命令获取所有内存利用了相关数据。
     * <p>
     * 如果我们没有配置maxmemory（可以通过config get/set maxmemory查询并在不重启Redis实例的前提下设置），
     * 那么Redis可能会耗尽服务器所有可用内存，从而可能导致swap甚至被系统kill掉。
     * <p>
     * 所以建议方案是配置maxmemory，并且配置maxmemory-policy（不要是默认的noviction）。
     * 即使这样还不够，因为如果并发比较大的话，缓存逐除策略可能会忙不过来，从而依然会有无法操作Redis的错误。
     * 所以强烈建议：在配置maxmemory-policy和maxmemory双策略的前提下，对used_memory进行监控，建议是maxmemory的90%。
     * 例如maxmemory为10G，那么当used_memory达到9G的时候，进行相关预警，从而准备扩容。
     * <p>
     * <li>used_memory: Redis分配的内存总量，即存储的所有数据占用的内存
     * <li>used_memory_human: 以可读格式返回使用的内存量
     * <li>used_memory_rss:从系统角度,显示Redis进程占用的物理内存总量
     * <li>used_memory_rss_human:以可读格式返回Redis进程占用的物理内存总量
     * <li>used_memory_peak:内存使用的最大值，表示used_memory峰值
     * <li>used_memory_peak_human:以可读格式返回内存使用的最大值
     * <li>total_system_memory:系统总内存
     * <li>total_system_memory_human:以可读格式返回系统总内存
     * <li>used_memory_lua:Lua进程使用内存
     * <li>used_memory_lua_human:以可读格式返回Lua进程使用内存
     * <li>mem_fragmentation_ratio:内存碎片率，等价于(used_memory_rss /used_memory)
     * <li>mem_allocator:redis使用的内存分配器
     * <p>
     * 重点关注used_memory以及used_memory_rss和碎片率mem_fragmentation_ratio
     * mem_fragmentation_ratio > 1:
     * (used_memory_rss-used_memory)多出部分没有存储数据，而是被内存碎片消耗了，如果差的很大，说明碎片很严重。
     * mem_fragmentation_ratio < 1: 一般出现在操作系统把Redis内存交换到硬盘导致。
     * <p>
     * <h1>内存消耗划分</h1> Redis进程内的内存主要包括： 自身内存 + 对象内存 + 缓冲内存+ 内存碎片，
     * 其中Redis自身内存消耗的很少，通常used_memory_rss在3MB左右，used_memory在800k左右。
     * <li>对象内存：对象内存是Redis中最占内存的一块，存储着用户所有数据。Redis所有数据都采用key-value，每次你创建key-value,都是创建2个对象，即key对象和value对象，
     * Key对象都是字符串，我们应当避免使用过长的key. Value对象根据使用情况不同占用内存也不尽相同，注意监控。
     * <li>缓冲内存： 缓冲内存涉及到客户端缓冲区，复制积压缓冲区和AOF缓冲区。
     * 客户端缓冲：指的是所有连接到Redis的服务器tcp连接输入输出缓冲，输入缓冲无法控制，最大空间1G；输出缓冲可通过client-output-buffer-limit控制。
     * # 普通客户端：client-output-buffer-limit normal 0 0 0
     * 普通客户端默认并没有对输出缓冲区做限制。但是如果当有大量的慢连接客户端接入时，这部分消耗就不能忽略了，因为消费的很慢，在成输出缓冲区数据积压。所以可以设置maxclients做限制。
     * # 从客户端：client-output-buffer-limit slave 256mb 64mb 60
     * 主节点会每一个从节点单独建立一条连接用于命令复制。当主节点网络延迟较高或主节点挂载大量的从节点时，这部分内存消耗将占用很大一部分，建议主节点挂载从节点最好不要超过2个。
     * # 订阅客户端：client-output-buffer-limit pubsub 32mb 8mb 60
     * 当生产消息的速度快于消费的速度时，输出缓冲区容易积压消息
     * 复制积压缓冲区：一个可重用的固定大小缓冲区用于实现部分复制功能，根据repl-backlog-size参数控制，默认1MB.
     * 对于复制积压缓区，主节点有一个，所有从节点啊共享这个缓冲区，因此可以设置较大的值，比如100MB,这部分投入是有价值的，可以有效避免全量复制。
     * AOF缓冲区：用于AOF重写期间保存最近写入的命令，等待被刷到磁盘
     * <h1>1.2.3 内存碎片</h2>
     * Redis默认的内存分配器是jemalloc，内存分配器的作用就是为了更好的管理和重复利用内存。但是当存储的数据长短差异较大的时候，以下场景容易出现高内存碎片问题：
     * <li># 频繁更新，对已经存在的key进行append setrange操作
     * <li># 大量过期键删除，键对象过期删除后，释放的 空间无法得到充分利用
     * <p>
     * 解决办法：
     * <li># 尽量数据对齐，视业务情况而定
     * <li># 安全重启：重启可以做到内存碎片重新整理
     * <h1>1.3 子进程内存消耗</h1>
     * 子进程内存主要是指AOF/RDB重写时Redis创建的子进程内存消耗。Redis执行fork操作产生子进程内存占用对外表现为与父进程相同，
     * 理论上需要一倍的相同物理内存来完成重写操作。但是Linux的copy-on-write机制使得父子进程共享相同的物理内存页，
     * 当父进程处理写请求时会对需要修改的页复制出一份副本完成写操作，而子进程依旧读取fork时整个父进程内存快照。
     * 
     * # Redis子进程并不需要消耗1倍 的父进程内存，但是依然要预留一些内存防止内存溢出
     * 
     * # 需要设置sysctl
     * vm.overcommit_memory=1允许内核可以分配所有的物理内存，防止Redis进程执行fork时因系统剩余内存不足而失败。
     * 
     * # 排查当前系统是否支持并开启THP，如果开启，建议关闭。
     */
    @Test
    public void infoMemory() {
        try (Jedis jedis = JedisDemo.newJedis()) {
            System.out.println("----------------------------命令：info Memory");
            System.out.println(jedis.info("Memory"));
        }
    }

    /**
     * 获取服务端信息
     */
    @Test
    public void infoPersistence() {
        try (Jedis jedis = JedisDemo.newJedis()) {
            System.out.println("----------------------------命令：info Persistence");
            System.out.println(jedis.info("Persistence"));
        }
    }

    /**
     * 吞吐量包括Redis实例历史总吞吐量，以及每秒钟的吞吐量。可以通过命令info stats中的几个得到我们要监控的吞吐量：
     * 
     * <pre>
    # 从Rdis上一次启动以来总计处理的命令数
    <li>total_commands_processed:2255
    # 当前Redis实例的OPS
    <li>instantaneous_ops_per_sec:12
    # 网络总入量
    <li>total_net_input_bytes:34312
    # 网络总出量
    <li>total_net_output_bytes:78215
    # 每秒输入量，单位是kb/s 
    <li> instantaneous_input_kbps:1.20
    # 每秒输出量，单位是kb/s
    <li> instantaneous_output_kbps:2.62
     * </pre>
     * 
     * 缓存命中率表示缓存的使用效率，很明显，它通过公式：HitRate = keyspace_hits / (keyspace_hits +
     * keyspace_misses)计算得到。缓存命中率越低，或导致越多的请求穿透Redis从MySQL（或者其他速度远比Redis慢的存储服务）获取数据，
     * 从而导致越多的请求有更大的延迟，导致API耗时增加，影响用户体验。
     * 
     * 如果是内存不足，那么需要扩容。例如info stats中的evicted_keys不为0，或者used_memory达到了内存上限。
     * 如果是用法问题，那么需要优化代码。
     * <li>拒绝的连接个数(rejected_connections):redis连接个数达到maxclients限制，拒绝新连接的个数
     * <li>新创建连接个数 (total_connections_received):
     * 如果新创建连接过多，过度地创建和销毁连接对性能有影响，说明短连接严重或连接池使用有问题，告警。
     * 
     * 
     * <pre>
     * # Stats
    total_connections_received:80921470
    total_commands_processed:4264255376
    instantaneous_ops_per_sec:58
    total_net_input_bytes:420732864042
    total_net_output_bytes:249967903528
    instantaneous_input_kbps:6.89
    instantaneous_output_kbps:0.50
    rejected_connections:0
    sync_full:1
    sync_partial_ok:0
    sync_partial_err:0
    expired_keys:79282080
    expired_stale_perc:0.18
    expired_time_cap_reached_count:12784
    evicted_keys:65158850
    keyspace_hits:515095497
    keyspace_misses:301132653
    pubsub_channels:0
    pubsub_patterns:0
    latest_fork_usec:13740
    migrate_cached_sockets:0
    slave_expires_tracked_keys:0
    active_defrag_hits:0
    active_defrag_misses:0
    active_defrag_key_hits:0
    active_defrag_key_misses:0
     * </pre>
     */
    @Test
    public void infoStats() {
        try (Jedis jedis = JedisDemo.newJedis()) {
            System.out.println("----------------------------命令：info Stats");
            System.out.println(jedis.info("Stats"));
        }
    }

    /**
     * 获取服务端信息
     */
    @Test
    public void infoReplication() {
        try (Jedis jedis = JedisDemo.newJedis()) {
            System.out.println("----------------------------命令：info Replication");
            System.out.println(jedis.info("Replication"));
        }
    }

    /**
     * 获取服务端CPU信息：CPU通常来说有三种状态：
     * <li>Idle, 空闲，什么也不做.
     * <li>Running a user space program,运行在用户态，例如shell或者chrome浏览器等用户态程序。
     * <li>Running the kernel, 运行在内核态，管理中断或者资源，执行系统调用，例如用户态程序malloc内存，fork子进程等.
     * 
     * <pre>
     * # CPU
    used_cpu_sys:703647.208364
    used_cpu_user:379222.900356
    used_cpu_sys_children:82479.762170
    used_cpu_user_children:287424.477837
     * </pre>
     * 
     * <li>used_cpu_sys_children: System CPU consumed by the background processes
     * Redis后台进程在内核态消耗的CPU，比如bgsave是fork一个子进程在后台去做。
     * <li>used_cpu_user_children: User CPU consumed by the background processes
     * Redis后台进程在用户态消耗的CPU。
     */
    @Test
    public void infoCPU() {
        try (Jedis jedis = JedisDemo.newJedis()) {
            System.out.println("----------------------------命令：info CPU");
            System.out.println(jedis.info("CPU"));
        }
    }

    /**
     * 获取服务端信息
     */
    @Test
    public void infoKeyspace() {
        try (Jedis jedis = JedisDemo.newJedis()) {
            System.out.println("----------------------------命令：info Keyspace");
            System.out.println(jedis.info("Keyspace"));
        }
    }

}
